package com.google.code.rgbgoesbetween.services;


import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import android.util.Log;

import com.google.code.rgbgoesbetween.data.AccountStatus;
import com.google.code.rgbgoesbetween.data.Profile;
import com.google.code.rgbgoesbetween.data.QueryStatus;

/**
 * Singleton with the data store and handles the cache. 
 * @author Patrick
 *
 */
public class SessionManager {

	private static final int MAX_NUM_OF_TRIES = 3;
	// Singleton items/methods
	private static SessionManager sessionManager;
	private SessionManager() { }
	
	/**
	 * 
	 * @return The singleton that handles the session.
	 */
	public static SessionManager getSessionManager() {
		if (sessionManager == null)
			sessionManager = new SessionManager();
		return sessionManager;
	}
	
	// SessionManager information
	private Profile profile;
	private PasswordHasher passHasher;
	
	// Map to store usernames and invalid login tries
	Map<String, Integer> invalidTries = new HashMap<String, Integer>();
	
	/**
	 * 
	 * Creates a profile iff the profile does not already exist and was not recently deleted. If a profile is found, STANDARD_MEMBER is returned.
	 * If the profile failed then DNE is returned. If the profile was deleted then deleted is returned.
	 * 
	 * @param username The name of the user's account
	 * @param password The password of the account
	 * @return The account status of the input account (may already exist !)
	 */
	public AccountStatus createProfile(String username, String password) {
		Profile profile = DataProvider.getDataProvider().
				createProfile(username.toLowerCase(Locale.US), passHasher.hashPassword(password));
		if(profile == null) {
			return AccountStatus.MEMBER_DNE;
		}
		if (AccountStatus.MEMBER_DELETED.equals(profile.getAccountStatus())) {
			return AccountStatus.MEMBER_DELETED;
		}
		return AccountStatus.STANDARD_MEMBER;
	}
	
	/**
	 * Creates an admin profile iff the profile does not already exist and was not recently deleted. If a profile is found, STANDARD_MEMBER is returned.
	 * If the profile failed then DNE is returned.
	 * 
	 * @param username The username of the admin
	 * @param password The admin's password
	 * 
	 * @return DNE if failed, ADMIN_MEMBER if succeeded
	 */
	public AccountStatus createAdminProfile(String username, String password) {
		Profile profile = DataProvider.getDataProvider().
				createAdminProfile(username.toLowerCase(Locale.US), passHasher.hashPassword(password));
		if(profile == null) {
			return AccountStatus.MEMBER_DNE;
		}
		return AccountStatus.ADMIN_MEMBER;
	}
	
	/**
	 * Given user login credentials, attempts to log in the specified user and stores the user's data
	 * in the local cache. Fails if the password hashes do not match. Will lock out the user if there are 3 consecutive
	 * failed attempts.
	 * 
	 * @param username The name of the user's profile
	 * @param password The user's password
	 * @return The account status of the given profile
	 */
	public AccountStatus login(String username, String password) {
		username = username.toLowerCase(Locale.US);
		String passHash = passHasher.hashPassword(password);
		Log.e("SessionManager", "user = " + username);
		Log.e("SessionManager", "passhash = " + passHash);
		Profile requestedProfile = DataProvider.getDataProvider().getProfile(username);
		Log.e("SessionManager", "Returned profile: " + requestedProfile);
		// If there is no username
		if (requestedProfile == null) { 
			return AccountStatus.MEMBER_DNE;
		} else { 
			// If user locked or deleted, we're done
			if (AccountStatus.LOCKED_MEMBER.equals(requestedProfile.getAccountStatus()) ||
					AccountStatus.MEMBER_DELETED.equals(requestedProfile.getAccountStatus())) {
				this.profile = requestedProfile;
				return requestedProfile.getAccountStatus();
			}
			// Username exists, but credentials invalid.
			else if (!passHash.equals(requestedProfile.getPassHash())) {
				// Invalid attempts per username increment by 1.
				invalidTries.put(username, invalidTries.containsKey(username) ? 
								invalidTries.get(username) + 1:  1);
				// If there have been 3 bad attempts for this user, change his account status
				// to locked AND immediately change this value in the Datastore.
				return manageBadLogin(requestedProfile);
			} 
			// Otherwise, return this profile's value. If profile locked out, even
			// if credentials are correct, we do not want to proceed.
			else {
				this.profile = requestedProfile;
				Log.e("SessionManager", "Set profile to: " + profile);
				invalidTries.put(username, 0); // Reset the tries
				return requestedProfile.getAccountStatus();	
			}
		}
	}
	
	/**
	 * This method decides whether to lockout a user or not after a set number
	 * of attempted logins.
	 * 
	 * @param p A profile
	 * @return Either an AccountStatus.INVLID or AccountStatus.LOCKED
	 * @throws RuntimeException if there is a failure to get the profile.
	 */
	private AccountStatus manageBadLogin(Profile p) {
		if (invalidTries.containsKey(p.getUsername()) && 
				invalidTries.get(p.getUsername()).intValue() >= MAX_NUM_OF_TRIES) {
			p.setAccountStatus(AccountStatus.LOCKED_MEMBER);
			if (updateProfile(p) != QueryStatus.QUERY_SUCCESS) 
				throw new RuntimeException("The profile could not be updated.");
			return p.getAccountStatus();
		} // If not locked out, specify invalid.
		else {
			return AccountStatus.MEMBER_INVALID;
		}

	}
	/**
	 * Clears the current profile logged into the system.
	 * @return An arbitrary int code.
	 */
	public int logout() {
		profile = null;
		return 0;
	}
	/**
	 * 
	 * @return The profile currently logged in.
	 */
	public Profile getProfile() {
		return profile;
	}
	/**
	 * 
	 * @return The username of the currently logged in user, or <code>null</code> if there is no one logged in
	 */
	public String getCurrentUsername() {
		return profile == null ? null : profile.getUsername();
	}
	
	/**
	 * Resets the invalid login strike counter.
	 * 
	 * @param username The username to clear
	 */
	public void clearInvalidTries(String username) {
		invalidTries.put(username, 0);
	}
	
	/**
	 * This will update the profile in the datastore with the same username as the profile given.
	 * 
	 * @param profile The new data of the profile
	 * @return A querey status enumeration representing the success (or failure) of the update
	 */
	public QueryStatus updateProfile(Profile profile) {
		return DataProvider.getDataProvider().updateProfile(profile);
	}
		
	/**
	 * Initializes the singleton for production use. It will hash passwords with a real hash function.
	 */
	public void init() {
		this.passHasher = new SHA1PassHasher();
	}

	/**
	 * Initializes the singleton for test use. It will hash passwords with a dummy pass hasher.
	 */
	public void testInit() {
		this.passHasher = new PasswordHasher() {	
			@Override
			public String hashPassword(String password) {
				return password;
			}
		};
	}	
}
